msg_tool\scripts\cat_system\archive/
int.rs

1//! CatSystem2 Archive File (.int)
2use super::twister::MersenneTwister;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::blowfish::Blowfish;
7use crate::utils::crc32::CRC32NORMAL_TABLE;
8use crate::utils::encoding::{decode_to_string, encode_string};
9use anyhow::Result;
10use overf::wrapping;
11use std::io::{Read, Seek, SeekFrom};
12use std::sync::{Arc, Mutex};
13
14pub use super::int_password::get_password_from_exe;
15
16#[derive(Debug)]
17/// Builder for CatSystem2 Archive scripts.
18pub struct CSIntArcBuilder {}
19
20impl CSIntArcBuilder {
21    /// Creates a new instance of `CSIntArcBuilder`.
22    pub fn new() -> Self {
23        CSIntArcBuilder {}
24    }
25}
26
27impl ScriptBuilder for CSIntArcBuilder {
28    fn default_encoding(&self) -> Encoding {
29        Encoding::Cp932
30    }
31
32    fn default_archive_encoding(&self) -> Option<Encoding> {
33        Some(Encoding::Cp932)
34    }
35
36    fn build_script(
37        &self,
38        data: Vec<u8>,
39        filename: &str,
40        _encoding: Encoding,
41        archive_encoding: Encoding,
42        config: &ExtraConfig,
43        _archive: Option<&Box<dyn Script>>,
44    ) -> Result<Box<dyn Script + Send + Sync>> {
45        Ok(Box::new(CSIntArc::new(
46            MemReader::new(data),
47            archive_encoding,
48            config,
49            filename,
50        )?))
51    }
52
53    fn build_script_from_file(
54        &self,
55        filename: &str,
56        _encoding: Encoding,
57        archive_encoding: Encoding,
58        config: &ExtraConfig,
59        _archive: Option<&Box<dyn Script>>,
60    ) -> Result<Box<dyn Script + Send + Sync>> {
61        if filename == "-" {
62            let data = crate::utils::files::read_file(filename)?;
63            Ok(Box::new(CSIntArc::new(
64                MemReader::new(data),
65                archive_encoding,
66                config,
67                filename,
68            )?))
69        } else {
70            let f = std::fs::File::open(filename)?;
71            let reader = std::io::BufReader::new(f);
72            Ok(Box::new(CSIntArc::new(
73                reader,
74                archive_encoding,
75                config,
76                filename,
77            )?))
78        }
79    }
80
81    fn build_script_from_reader<'a>(
82        &self,
83        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
84        filename: &str,
85        _encoding: Encoding,
86        archive_encoding: Encoding,
87        config: &ExtraConfig,
88        _archive: Option<&Box<dyn Script>>,
89    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
90        Ok(Box::new(CSIntArc::new(
91            reader,
92            archive_encoding,
93            config,
94            filename,
95        )?))
96    }
97
98    fn extensions(&self) -> &'static [&'static str] {
99        &["int"]
100    }
101
102    fn script_type(&self) -> &'static ScriptType {
103        &ScriptType::CatSystemInt
104    }
105
106    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
107        if buf_len >= 4 && buf.starts_with(b"KIF\0") {
108            return Some(10);
109        }
110        None
111    }
112
113    fn is_archive(&self) -> bool {
114        true
115    }
116}
117
118fn detect_script_type(buf: &[u8], buf_len: usize, _filename: &str) -> Option<&'static ScriptType> {
119    #[cfg(feature = "cat-system-img")]
120    if buf_len >= 4 && buf.starts_with(b"HG-3") {
121        return Some(&ScriptType::CatSystemHg3);
122    }
123    if buf_len >= 8 && buf.starts_with(b"CatScene") {
124        return Some(&ScriptType::CatSystem);
125    }
126    if buf_len >= 4 && buf.starts_with(b"CSTL") {
127        return Some(&ScriptType::CatSystemCstl);
128    }
129    None
130}
131
132#[derive(Clone, Debug)]
133struct CSIntFileHeader {
134    name: String,
135    offset: u32,
136    size: u32,
137}
138
139#[derive(Debug)]
140struct Entry<T: Read + Seek + std::fmt::Debug> {
141    header: CSIntFileHeader,
142    reader: Arc<Mutex<T>>,
143    pos: usize,
144    script_type: Option<ScriptType>,
145}
146
147impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for Entry<T> {
148    fn name(&self) -> &str {
149        &self.header.name
150    }
151
152    fn size(&self) -> Option<u64> {
153        Some(self.header.size as u64)
154    }
155
156    fn script_type(&self) -> Option<&ScriptType> {
157        self.script_type.as_ref()
158    }
159
160    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
161        Ok(Box::new(self))
162    }
163}
164
165impl<T: Read + Seek + std::fmt::Debug> Read for Entry<T> {
166    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
167        let mut reader = self.reader.lock().map_err(|e| {
168            std::io::Error::new(
169                std::io::ErrorKind::Other,
170                format!("Failed to lock mutex: {}", e),
171            )
172        })?;
173        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
174        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
175        if bytes_read == 0 {
176            return Ok(0);
177        }
178        let bytes_read = reader.read(&mut buf[..bytes_read])?;
179        self.pos += bytes_read;
180        Ok(bytes_read)
181    }
182}
183
184impl<T: Read + Seek + std::fmt::Debug> Seek for Entry<T> {
185    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
186        let new_pos = match pos {
187            SeekFrom::Start(offset) => offset as usize,
188            SeekFrom::End(offset) => {
189                if offset < 0 {
190                    if (-offset) as usize > self.header.size as usize {
191                        return Err(std::io::Error::new(
192                            std::io::ErrorKind::InvalidInput,
193                            "Seek from end exceeds file length",
194                        ));
195                    }
196                    self.header.size as usize - (-offset) as usize
197                } else {
198                    self.header.size as usize + offset as usize
199                }
200            }
201            SeekFrom::Current(offset) => {
202                if offset < 0 {
203                    if (-offset) as usize > self.pos {
204                        return Err(std::io::Error::new(
205                            std::io::ErrorKind::InvalidInput,
206                            "Seek from current exceeds current position",
207                        ));
208                    }
209                    self.pos.saturating_sub((-offset) as usize)
210                } else {
211                    self.pos + offset as usize
212                }
213            }
214        };
215        self.pos = new_pos;
216        Ok(self.pos as u64)
217    }
218
219    fn stream_position(&mut self) -> std::io::Result<u64> {
220        Ok(self.pos as u64)
221    }
222}
223
224struct MemEntry {
225    name: String,
226    data: MemReader,
227}
228
229impl Read for MemEntry {
230    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
231        self.data.read(buf)
232    }
233}
234
235impl ArchiveContent for MemEntry {
236    fn name(&self) -> &str {
237        &self.name
238    }
239
240    fn size(&self) -> Option<u64> {
241        Some(self.data.data.len() as u64)
242    }
243
244    fn script_type(&self) -> Option<&ScriptType> {
245        detect_script_type(&self.data.data, self.data.data.len(), &self.name)
246    }
247
248    fn data(&mut self) -> Result<Vec<u8>> {
249        Ok(self.data.data.clone())
250    }
251
252    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
253        Ok(Box::new(&mut self.data))
254    }
255}
256
257#[derive(Debug)]
258/// CatSystem2 Archive script.
259pub struct CSIntArc<'b, T: Read + Seek + std::fmt::Debug + 'b> {
260    reader: Arc<Mutex<T>>,
261    encrypt: Option<Blowfish>,
262    entries: Vec<CSIntFileHeader>,
263    _mark: std::marker::PhantomData<&'b ()>,
264}
265
266const NAME_SIZES: [usize; 2] = [0x20, 0x40];
267
268impl<'b, T: Read + Seek + std::fmt::Debug + 'b> CSIntArc<'b, T> {
269    /// Creates a new instance of `CSIntArc` from a reader.
270    ///
271    /// * `reader` - The reader to read the archive from.
272    /// * `archive_encoding` - The encoding used for the archive.
273    /// * `config` - Extra configuration options.
274    /// * `filename` - The name of the file.
275    pub fn new(
276        mut reader: T,
277        archive_encoding: Encoding,
278        config: &ExtraConfig,
279        _filename: &str,
280    ) -> Result<Self> {
281        let mut magic = [0u8; 4];
282        reader.read_exact(&mut magic)?;
283        if &magic != b"KIF\0" {
284            return Err(anyhow::anyhow!(
285                "Invalid magic number for CatSystem2 archive"
286            ));
287        }
288        let entry_count = reader.read_u32()?;
289        let mut keybuf = [0u8; 12];
290        reader.read_exact(&mut keybuf)?;
291        if &keybuf == b"__key__.dat\0" {
292            let key = match &config.cat_system_int_encrypt_password {
293                Some(password) => Self::get_key(password)?,
294                None => {
295                    return Err(anyhow::anyhow!(
296                        "CatSystem2 archive requires encryption password. Please use --cat-system-int-encrypt-password/--cat-system-int-exe option."
297                    ));
298                }
299            };
300            eprintln!("Using CatSystem2 archive encryption key: {key:08X}");
301            let seed = reader.peek_u32_at(0x4C)?;
302            let mut twister = MersenneTwister::new(seed);
303            let blowfish_key = twister.rand().to_le_bytes();
304            let encrypt = match Blowfish::new(&blowfish_key) {
305                Ok(bf) => bf,
306                Err(e) => {
307                    return Err(anyhow::anyhow!("Failed to create Blowfish cipher: {}", e));
308                }
309            };
310            let mut entries = Vec::with_capacity(entry_count as usize - 1);
311            let mut name_buf = [0u8; 0x40];
312            reader.seek(SeekFrom::Start(0x50))?;
313            for i in 1..entry_count {
314                reader.read_exact(&mut name_buf)?;
315                let offset = reader.read_u32()? + i;
316                let size = reader.read_u32()?;
317                let decryped = encrypt.decrypt([offset, size]);
318                twister.s_rand(key + i);
319                let name_key = twister.rand();
320                let name = Self::decrypt_name(&mut name_buf, name_key, archive_encoding)?;
321                let entry = CSIntFileHeader {
322                    name,
323                    offset: decryped[0],
324                    size: decryped[1],
325                };
326                entries.push(entry);
327            }
328            return Ok(CSIntArc {
329                reader: Arc::new(Mutex::new(reader)),
330                encrypt: Some(encrypt),
331                entries: entries,
332                _mark: std::marker::PhantomData,
333            });
334        }
335        let file_size = reader.seek(SeekFrom::End(0))?;
336        let mut entries = Vec::with_capacity(entry_count as usize);
337        for size in NAME_SIZES {
338            reader.seek(SeekFrom::Start(0x8))?;
339            for _ in 0..entry_count {
340                let name = reader.read_fstring(size, archive_encoding, true)?;
341                if name.is_empty() {
342                    entries.clear();
343                    break;
344                }
345                let current_offset = reader.stream_position()?;
346                let offset = reader.read_u32()?;
347                let size = reader.read_u32()?;
348                if offset as u64 <= current_offset
349                    || !((offset as u64) < file_size
350                        && size as u64 <= file_size
351                        && offset as u64 <= file_size as u64 - size as u64)
352                {
353                    entries.clear();
354                    break;
355                }
356                let entry = CSIntFileHeader { name, offset, size };
357                entries.push(entry);
358            }
359            if !entries.is_empty() {
360                return Ok(CSIntArc {
361                    reader: Arc::new(Mutex::new(reader)),
362                    encrypt: None,
363                    entries,
364                    _mark: std::marker::PhantomData,
365                });
366            }
367        }
368        Err(anyhow::anyhow!(
369            "Failed to parse archives. Maybe another name length is used? (expected 0x20 or 0x40)",
370        ))
371    }
372
373    fn decrypt_name(name: &mut [u8; 0x40], key: u32, encoding: Encoding) -> Result<String> {
374        let mut k = wrapping! {((key >> 24) + (key >> 16) + (key >> 8) + key) & 0xFF};
375        let mut i = 0;
376        while i < 0x40 && name[i] != 0 {
377            let v = name[i];
378            if v.is_ascii_alphabetic() {
379                let mut j = if v.is_ascii_lowercase() {
380                    b'z' - v
381                } else {
382                    b'Z' - v + 26
383                } as i8;
384                j -= (k % 0x34) as i8;
385                if j < 0 {
386                    j += 0x34;
387                }
388                j = 0x33 - j;
389                name[i] = if j < 26 {
390                    b'z' - j as u8
391                } else {
392                    b'Z' - (j as u8 - 26)
393                };
394            }
395            k += 1;
396            i += 1;
397        }
398        decode_to_string(encoding, &name[..i], true)
399    }
400
401    fn get_key(password: &str) -> Result<u32> {
402        let bytes = encode_string(Encoding::Cp932, password, true)?;
403        let mut key = 0xFFFFFFFF;
404        for &c in bytes.iter() {
405            key = !CRC32NORMAL_TABLE[((key >> 24) ^ c as u32) as usize] ^ (key << 8);
406        }
407        Ok(key)
408    }
409}
410
411impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for CSIntArc<'b, T> {
412    fn default_output_script_type(&self) -> OutputScriptType {
413        OutputScriptType::Json
414    }
415
416    fn default_format_type(&self) -> FormatOptions {
417        FormatOptions::None
418    }
419
420    fn is_archive(&self) -> bool {
421        true
422    }
423
424    fn iter_archive_filename<'a>(
425        &'a self,
426    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
427        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
428    }
429
430    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
431        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
432    }
433
434    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
435        if index >= self.entries.len() {
436            return Err(anyhow::anyhow!(
437                "Index out of bounds: {} (max: {})",
438                index,
439                self.entries.len()
440            ));
441        }
442        let entry = &self.entries[index];
443        let mut entry = Entry {
444            header: entry.clone(),
445            reader: self.reader.clone(),
446            pos: 0,
447            script_type: None,
448        };
449        if let Some(encrypt) = &self.encrypt {
450            let mut data = entry.data()?;
451            entry.pos = 0;
452            for i in 0..data.len() / 8 {
453                let j = i * 8;
454                let l = data[j] as u32
455                    | (data[j + 1] as u32) << 8
456                    | (data[j + 2] as u32) << 16
457                    | (data[j + 3] as u32) << 24;
458                let r = data[j + 4] as u32
459                    | (data[j + 5] as u32) << 8
460                    | (data[j + 6] as u32) << 16
461                    | (data[j + 7] as u32) << 24;
462                let result = encrypt.decrypt([l, r]);
463                data[j..j + 4].copy_from_slice(&result[0].to_le_bytes());
464                data[j + 4..j + 8].copy_from_slice(&result[1].to_le_bytes());
465            }
466            return Ok(Box::new(MemEntry {
467                name: entry.header.name.clone(),
468                data: MemReader::new(data),
469            }));
470        }
471        let mut buf = [0u8; 32];
472        let buf_len = match entry.read(&mut buf) {
473            Ok(len) => len,
474            Err(e) => {
475                return Err(anyhow::anyhow!(
476                    "Failed to read entry '{}': {}",
477                    entry.header.name,
478                    e
479                ));
480            }
481        };
482        entry.pos = 0;
483        entry.script_type = detect_script_type(&buf, buf_len, &entry.header.name).copied();
484        Ok(Box::new(entry))
485    }
486}